<?php

/**
 * Class for viewing any HTML, or XML element. This includes comments, or text strings.
 * The code within the element is an array of such elements.
 *
 * @author Ian Clarence ian.clarence@gmail.com
 */ 
class ViewHtmlElement extends Component {

    /**
     * What string is considered a tab.
     */
    const TAB_STRING = "    ";

    /**
     * Flag indicating whether there is any whitespace in the inner HTML.
     */
    const NO_WHITESPACE = FALSE;

    /**
     * If this element is an inner HTML element text string around which there is no whitespace, then this
     * property needs to be set to TRUE, FALSE otherwise.
     *
     * @var bool
     */
    protected $noWhitespace;

    /**
     * The indentation of the element in number of tabs.
     *
     * @var int
     */
    protected $indent = 0;

    /**
     * The inner HTML within this element: an array of other ViewHtmlElement objects.
     *
     * @var array
     */
    protected $innerHtml = array();

    /**
     * The constructor.
     *
     * @param array $fields
     */
    public function __construct(array $fields = array()) {
        
        /**
         * If the tag field is not set, then set it to "div" by default.
         */
        $fields["attributes"] = isset($fields["attributes"]) ? $fields["attributes"] : array();
        
        parent::__construct($fields);
    }

    /**
     * Assigning the id attribute from the element.
     *
     * @param string $elementId
     */
    public function setElementId($elementId) {
        $this->fields["element_id"] = $elementId;
    }

    /**
     * Removing the id attribute from the element.
     */
    public function unsetElementId() {
        unset($this->fields["element_id"]);
    }

    /**
     * Setting the text string value.
     *
     * @param string $textString
     */
    public function setTextString($textString) {
        $this->fields["is_text"] = TRUE;
        $this->fields["text_string"] = $textString;
    }

    /**
     * Unsetting the text string value.
     */
    public function unsetTextString() {
        $this->fields["is_text"] = FALSE;
        $this->fields["text_string"] = NULL;
    }

    /**
     * Setting a new attribute to the element, or renaming an existing attribute, named "$attributeName".
     *
     * @param string $attributeName
     * @param string $attributeValue
     */
    public function setAttribute($attributeName, $attributeValue) {
        if (!isset($this->fields["attributes"])) {
            $this->fields["attributes"] = array();
        }
        if ($attributeValue != NULL) {
            $this->fields["attributes"][$attributeName] = $attributeValue;
        }
    }

    /**
     * Getting the value of the attribute named "$attributeName".
     *
     * @param string $attributeName
     * @return string
     */
    public function getAttribute($attributeName) {
        if (!isset($this->fields["attributes"][$attributeName])) {
            return NULL;
        }
        return $this->fields["attributes"][$attributeName];
    }

    /**
     * Removing the attribute named "$attributeName".
     *
     * @param string $attributeName
     */
    public function unsetAttribute($attributeName) {
        if (isset($this->fields["attributes"][$attributeName])) {
            unset($this->fields["attributes"][$attributeName]);
        }
    }

    /**
     * Determining whether there is a class named "$className".
     *
     * @param string $className
     * @return bool
     */
    private function classSet($className) {
        foreach($this->fields["class_array"] as $value) {
            if ($value == $className) {
                return TRUE;
            }
        }
        return FALSE;
    }

    /**
     * Adding a class "$className".
     *
     * @param string $className
     */
    public function addClass($className) {
        if (!isset($this->fields["class_array"])) {
            $this->fields["class_array"] = array();
        }
        $this->fields["class_array"][$className] = $className;
    }

    /**
     * Removing the class "$className".
     *
     * @param string $className
     */
    public function removeClass($className) {
        unset($this->fields["class_array"][$className]);
    }

    /**
     * Removing all the classes.
     */
    public function resetClassArray() {
        $this->fields["class_array"] = array();
    }

    /**
     * Add odd or even class.
     * Useful in tables etc.
     *
     * @param int $key
     */
    public function addOddOrEvenClass($key) {
        if ($key % 2 == 0) {
            if ($this->classSet("odd")) {
                $this->removeClass("odd");
            }
            $this->addClass("even");
        }
        else {
            if ($this->classSet("even")) {
                $this->removeClass("even");
            }
            $this->addClass("odd");
        }
    }

    /**
     * Adding another element to the inner HTML, either to the beginning (default) or to the end.
     *
     * @param mixed $innerHtmlElement
     * @param bool $addToBeginning
     * @return bool
     */
    public function addInnerHtml($innerHtmlElement, $addToBeginning = FALSE) {
        if ($addToBeginning) {
            array_unshift($this->innerHtml, $innerHtmlElement);
        }
        else {
            array_push($this->innerHtml, $innerHtmlElement);
        }
        return TRUE;
    }

    /**
     * Inserting a simple element, like <br /> or <p> with text.
     *
     * @param string $tag
     * @param string $textString
     * @param bool $addToBeginning
     * @return bool
     */
    public function elementAdd(
        $tag = "br", 
        $textString = NULL, 
        $addToBeginning = FALSE
    ) {
        return $this->addInnerHtml(self::elementInit($tag, $textString), $addToBeginning);
    }

    /**
     * Getting the indentation of the element as a string.
     *
     * @return string
     */
    protected function getIndentation() {
        $output = "";
        for ($key = 0; $key < $this->indent; $key++) {
            $output .= self::TAB_STRING;
        }
        return $output;
    }

    /**
     * Setting the indentation of the element.
     *
     * @param int $indent
     */
    protected function setIndent($indent) {
        $this->indent = $indent;
    }

    /**
     * Encapsulating the existing element in another one, div by default.
     * Returns a clone of this element, encapsulated.
     *
     * @param string $tag
     * @return ViewHtmlElement
     */
    public function encapsulate(
        $tag = "div", 
        $id = NULL, 
        $classArray = array(), 
        $attributes = array()
    ) {
        $element =  new self(array(
            "tag" => $tag,
            "element_id" => $id,
            "class_array" => $classArray,
            "attributes" => $attributes
        ));
        $element->addInnerHtml(clone $this);
        return $element;
    }

    /**
     * Remove the encapusulation if possible, otherwise just return a clone of this element.
     *
     * @param string $tag
     * @return ViewHtmlElement
     */
    public function unencapsulate() {
        if (count($this->innerHtml) == 1 && $this->innerHtml instanceof ViewHtmlElement) {
            return $this->innerHtml[0];
        }
        return clone $this;
    }

    /**
     * Putting all the attributes into the attributes array.
     */
    private function buildAttributes() {

        /**
         * The id attribute:
         * To avoid confustion with other fields labelled "id", this must be input as a field of index "element_id"
         * in order to become the "id" attribute of the element.
         */
        if (isset($this->fields["element_id"])) {
            $this->fields["attributes"]["id"] = $this->getElementId();
        }

        /**
         * Class array.
         */
        if (isset($this->fields["class_array"])) {
            if (count($this->fields["class_array"]) > 0) {
                $this->fields["attributes"]["class"] = implode(" ", $this->fields["class_array"]);
            }
        }
    }

    /**
     * Making an element a text string.
     *
     * @param string $textString
     */
    public function makeText($textString) {
        $this->fields["is_text"] = TRUE;
        $this->fields["text_string"] = $textString;
    }

    /**
     * Adding a textstring to the inner HTML.
     *
     * @param string $textString
     */
    public function addText($textString) {
        $textElement = new ViewHtmlElement();
        $textElement->makeText($textString);
        $this->addInnerHtml($textElement);
    }

    /**
     * Creating a text element
     *
     * @param string $textString
     */
    public static function textInit($textString) {
        $textElement = new ViewHtmlElement();
        $textElement->makeText($textString);
        return $textElement;
    }

    /**
     * Returns an HTML object with inner HTML already added, and an optional attribute.
     *
     * @param string $tag
     * @param mixed $content
     * @param string $attributeName
     * @param string $attributeValue
     * @return ViewHtmlElement
     */
    public static function elementInit(
        $tag, 
        $content = NULL, 
        $attributeName = NULL, 
        $attributeValue = NULL
    ) {
        $element = new self(array("tag" => $tag));
        if (is_array($content) or $content == NULL) {
            $element->setInnerHtml($content);
        }
        else if ($content instanceof ViewHtmlElement) {
            $element->addInnerHtml($content);
        }
        else {
            $element->addText($content);
        }
        if ($attributeName != NULL) {
            $element->setAttribute($attributeName, $attributeValue);
        }
        return $element;
    }
    
    /**
     * Convert an associative array to an XML string.
     * Works recursively.
     * 
     * @param array $array
     * @param string $name 
     */
    public function arrayToXml($array, $name = 'data') {
        $this->fields['tag'] = strtolower($name);
        if (is_array($array)) {
            foreach ($array as $key => $value) {
                if (!is_null($value) && $value != array()) {
                    if (($key == 'id' || $key == 'name') && (!is_array($value))) {
                        $this->setAttribute($key, $value);
                    }
                    else {
                        $elementName = is_numeric($key) ? $name . '_element' : $key;
                        $element = new self;
                        $element->arrayToXml($value, $elementName);
                        $this->addInnerHtml($element);
                    }
                }
            }            
        }
        else {
            $this->addInnerHtml(self::textInit($array));
        }
    }

    /**
     * Setter for the inner HTML.
     *
     * @param array $content
     */
    public function setInnerHtml($content = NULL) {
        if (is_array($content)) {
            $this->innerHtml = $content;
        }
        else if ($content == NULL) {
            $this->innerHtml = array();
        }
    }

    /**
     * Viewing the inner HTML.
     *
     * @param bool $noWhitespace
     * @return string
     */
    private function viewInnerHtml($noWhitespace = FALSE) {
        $output = '';
        if (!is_array($this->innerHtml)) {
            return $output;
        }
        foreach ($this->innerHtml as $element) {
            if ($element instanceof ViewHtmlElement) {
                $element->setIndent($this->indent + 1);
                $element->setNoWhitespace($noWhitespace);
                $output .= $element->view();
            }
        }
        return $output;
    }

    /**
     * Setting the no-whitespace property
     *
     * @param bool $noWhitespace
     */
    private function setNoWhitespace($noWhitespace = FALSE) {
        $this->noWhitespace = $noWhitespace;
    }

    /**
     * Viewing the entire element.
     *
     * @return string
     */	
    public function view() {
        $indentation = $this->getIndentation();

        /**
         * Text string
         */
        if (isset($this->fields["is_text"])) {
            if ($this->fields["is_text"]) {
                if ($this->noWhitespace) {
                    return $this->fields["text_string"];
                }
                $sentences = explode(". ", $this->fields["text_string"]);
                $output = $indentation . implode(".\n" . $indentation, $sentences);
                $output .= "\n";
                return $output;
            }
        }

        /**
         * XML declaration
         */
        if (isset($this->fields["is_xml_declaration"])) {
            if ($this->fields["is_xml_declaration"]) {
                return "<?xml version=\"1.0\" encoding=\"" . CHARSET . "\"?>\n";
            }
        }

        /**
         * Comment
         */
        if (isset($this->fields["is_comment"])) {
            if ($this->fields["is_comment"]) {
                return $indentation . "<!--\n" . $this->viewInnerHtml() . "-->\n";
            }
        }

        /**
         * Element
         */
        
        $this->buildAttributes();
        $separator = (count($this->fields["attributes"]) > 3) ? "\n" . $indentation . self::TAB_STRING : " ";
        $output = $indentation . "<" . strtolower($this->fields["tag"]);
        if (count($this->fields["attributes"]) > 0) {
            foreach ($this->fields["attributes"] as $key => $attribute) {
                $output .= $separator . strtolower($key) . " = \"" . $attribute . "\"";
            }
        }
        $output .= (count($this->innerHtml) == 0) ? " />\n" :
            ">" . (static::NO_WHITESPACE ? "" : "\n") . $this->viewInnerHtml(static::NO_WHITESPACE) .
            (static::NO_WHITESPACE ? "" : $indentation) . "</" . strtolower($this->fields["tag"]) . ">\n";
        return $output;
    }

}